package com.samp.lucene.core;

import java.nio.file.Paths;
import java.util.Date;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.document.DateTools.Resolution;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.DoublePoint;
import org.apache.lucene.document.IntPoint;
import org.apache.lucene.document.LongPoint;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.classic.MultiFieldQueryParser;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.queryparser.flexible.standard.StandardQueryParser;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MultiPhraseQuery;
import org.apache.lucene.search.PhraseQuery;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.RegexpQuery;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TermRangeQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.WildcardQuery;
import org.apache.lucene.search.spans.SpanNearQuery;
import org.apache.lucene.search.spans.SpanTermQuery;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.wltea.analyzer.lucene.IKAnalyzer;

/**
 * 参考：https://www.cnblogs.com/leeSmall/p/9027172.html  搜索流程详解
 * https://blog.csdn.net/xiaomin1991222/article/details/50981092 Field类及辅助类说明<br>
 * 1：如果字符串是用户输入的，应该使用查询解析器，如果是系统生成的或指定的，应使用基本查询API.
 * 2：没有分词的字段，应该使用基本查询API加入到最终查询中，而不要使用查询分析器(有用analyzer的都是分词的，并非只有中文分词)
 * 3：对于时间，数值等，应该使用对应基本查询API.
 * 
 * 
 * 1:各种不同的Query，首先要关注查询的字段的索引方式，比如wildcardQuery这类，最好不要分词<b>
 * 如果是分词的字段，要注意处理的是分词的产生的term。
 * @author EX-WANWEI001
 *
 */
public class QueryDemo {
	
	public static void searchDoc(String indexDir, String field, String searchTerm, Integer rows, String queryType){
		try {
			Directory directory = FSDirectory.open(Paths.get(indexDir));
			IndexReader ir = DirectoryReader.open(directory);
			IndexSearcher is = new IndexSearcher(ir);
			Query query = null;
			if( field != null) {
				query = getQuery(field, searchTerm, queryType);
			}else{
				query = getQuery( queryType);
			}
			TopDocs topDocs = is.search(query, rows);
			ScoreDoc[] hits = topDocs.scoreDocs;
			System.out.println("filed="+field+", searchTerm="+searchTerm+",quertType="+queryType);
			String prifix = "    ";
			System.out.println(prifix+"总共匹配记录总数："+topDocs.totalHits);
			System.out.println(prifix+"当前返回记录总数："+hits.length);
			for(int i = 0 ; i < hits.length ; i++ ){
				System.out.println(prifix+"docNum="+hits[i].doc+",docScore="+hits[i].score);
				Document doc = is.doc(hits[i].doc);
				printDoc(prifix, doc);
			}
			ir.close();
			directory.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	}
	
	public static void searchDoc(String indexDir,  Integer rows, String queryType){
		searchDoc(indexDir,null,null,rows,queryType);
	}
	
	private static void printDoc(String prifix, Document doc){
		String append="    ";
		System.out.println(prifix+append+"id="+doc.get("id"));
		System.out.println(prifix+append+"title="+doc.get("title"));
		System.out.println(prifix+append+"creator="+doc.get("creator"));
		Date date = new Date(Long.valueOf(doc.get("createTime")));
		System.out.println(prifix+append+"createTime="+doc.get("createTime"));
		System.out.println(prifix+append+"createTime="+DateTools.dateToString(date, Resolution.SECOND));
		System.out.println(prifix+append+"intNum="+doc.get("intNum"));
		System.out.println(prifix+append+"doubleNum="+doc.get("doubleNum"));
		Date lastModified =  new Date(Long.valueOf(doc.get("lastModified")));
		System.out.println(prifix+append+"lastModified="+DateTools.dateToString(lastModified, Resolution.SECOND));
		System.out.println(prifix+append+"path="+doc.get("path"));
	}
	
	/**
	 * 1:termQuery 按term查询，如果索引时，是StringField，需要全匹配，如果是TextField，按分词来匹配
	 * 2:Int,Double,Long等数值类型，可以用对应*Point实现精确查找或区间查找。
	 * 3:date的精确和区间查询，转换成long来查。
	 * 
	 * 注意，查询的时候，还要看索引时的类型，比如ID为int类型，就不能用TermsQuery查找，找不到的。
	 * @param field
	 * @param searchTerm
	 * @param queryType
	 * @return
	 * @throws ParseException
	 */
	private static Query getQuery(String field, String searchTerm, String queryType) throws Exception{
		Query query = null;
	    if( "termQuery".equals(queryType) ){
			query = new TermQuery(new Term(field, searchTerm));
		}else if( "intQuery".equals(queryType) ){
			query = IntPoint.newExactQuery(field, Integer.valueOf(searchTerm));
		}else if( "intQuery-range".equals(queryType) ){
			query = IntPoint.newRangeQuery(field, Integer.valueOf(searchTerm)-2, Integer.valueOf(searchTerm)+2);
		}else if( "doubleQuery-range".equals(queryType) ){
			query = DoublePoint.newRangeQuery(field, Double.valueOf(searchTerm)-0.3, Double.valueOf(searchTerm)+0.3);
		}else if( "longPoint".equals(queryType) ){
			query = LongPoint.newExactQuery(field, Long.valueOf(searchTerm));
			//query = LongPoint.newRangeQuery(field, Long.valueOf(searchTerm)-10, Long.valueOf(searchTerm)+10);
		}else if( "dateQuery".equals(queryType) ){
			Long dateVal = DateTools.stringToTime(searchTerm);
			query = LongPoint.newExactQuery(field, dateVal);
			//query = LongPoint.newRangeQuery(field, Long.valueOf(searchTerm)-10, Long.valueOf(searchTerm)+10);
		}else if( "booleanQuery".equals(queryType) ){
			Query query1 = new TermQuery(new Term("id", "1"));
			Query query2 = new TermQuery(new Term("id", "3"));
			BooleanQuery bq = new BooleanQuery.Builder().add(query1, Occur.SHOULD).add(query2, Occur.SHOULD).build();
			return bq;
		}else{
			throw new ParseException("unsupport queryType: "+queryType);
		}
		return query;
	}
	
	@SuppressWarnings("unused")
	private static Query getQuery( String queryType ) throws Exception{
		Query query = null;
		if( "booleanQuery".equals(queryType) ){
		  /*
			* boolean查询对应的几种组合：
			*  1．MUST和MUST：取得连个查询子句的交集。 
			*	2．MUST和MUST_NOT：表示查询结果中不能包含MUST_NOT所对应得查询子句的检索结果。 
			*	3．SHOULD与MUST_NOT：连用时，功能同MUST和MUST_NOT。
			*	4．SHOULD与MUST连用时，结果为MUST子句的检索结果,但是SHOULD可影响排序。
			*	5．SHOULD与SHOULD：表示“或”关系，最终检索结果为所有检索子句的并集。
			*	6．MUST_NOT和MUST_NOT：无意义，检索无结果。
			*参考：https://www.cnblogs.com/weipeng/archive/2012/04/18/2455079.html
			*/
			Query query1 = getQuery("doubleNum", "1.5", "doubleQuery-range");
			Query query2 = getQuery("id", "4", "intQuery-range");
			query = new BooleanQuery.Builder().add(query1, Occur.SHOULD).add(query2, Occur.SHOULD).build();
		}else if ("phraseQuery".equals(queryType)) {//百度网址大全
			/*
			 * 短语查询，并不是查询两个项的并集，而是多个词组成的一个短语，而且顺序也是按给定的顺序来匹配
			 * 中间可以间隔slop个项(term).
			 * 参考：https://blog.csdn.net/rick_123/article/details/6708527
			 * 短语查询中，要理解slop的概念：slop是指两个项的位置之间允许的最大间隔距离<br>
			 * 这里还要注意下中文分词的误区，如果是最细粒度分词，对于百度网址大全，百度会拆分成
			 * 百度，百，度，这样百度和网址之间就隔了两个项了（百和度），此时slop至少要为2才能查询出来。
			 * 对于智能分词，中间就没间隔了，slop就是0.
			 * 
			 * 而对于用Builder方法生成PhraseQuery，后面的参数position是指的相对位置，所以0,2和4,6完全是等价的，如果不带参数，默认递增1
			 * 相当于两个语是连着的，比如"网址大全"，而要搜索"百度**大全"，中间隔两个位置，所以position也要相关2.
			 */
			//PhraseQuery pq = new PhraseQuery(1, "title","百度", "大全");
			PhraseQuery pq = new PhraseQuery.Builder().add(new Term("title","网址"),0 ).add(new Term("title","大全"),2).build();
			query = pq;
		}else if ("multiPhraseQuery".equals(queryType)) {//百度网址大全
			/*
			 *多重短语查询,就是同一个位置，可以出现多个项，比如想查"网址大全"和"产品大全"，这种情况普通的短语查询就无法实现。
			 */
			Term[] terms = new Term[2];
			terms[0] = new Term("title", "网址");
			terms[1] = new Term("title", "产品");
			Term term = new Term("title", "大全");
			MultiPhraseQuery multiPhraseQuery = new MultiPhraseQuery.Builder() .add(terms).add(term).build();
			query = multiPhraseQuery;
		}else if ("spanTermQuery".equals(queryType)) {
			//SpanTermQuery查询效果类似于TermQuery，只不过是增加了查询结果中单词的距离信息，可供其它SpanQuery查询。
			//SpanQuery有很多子类实现，具体需要再研究。
			SpanTermQuery spanTermQuery = new SpanTermQuery(new Term("title", "大全"));
			query = spanTermQuery;
		}else if ("spanNearQuery".equals(queryType)) {//百度产品大全,百度网址大全
			/*
			 * 感觉SpanNearQuery与PhraseQuery类似
			 * 通过构造函数：SpanNearQuery(SpanQuery[] clausesIn, int slop, boolean inOrder)来生成SpanNearQuery，slop
			 * 与PhraseQuery意义相同，inOrder表示是否按数据顺序查找,true是顺序相关,false是顺序无关。
			 */
			SpanTermQuery tq1 = new SpanTermQuery(new Term("title", "百度"));
			SpanTermQuery tq2 = new SpanTermQuery(new Term("title", "大全"));
			SpanTermQuery tq3 = new SpanTermQuery(new Term("title", "网址"));
			//通过构造函数生成SpanNearQuery
			//SpanNearQuery nq = new SpanNearQuery(new SpanQuery[]{tq2,tq1}, 1, false);
			//通过Builder来生成函数,gap是与上一个条件的间隔，slop是整个query的最大间隔。
			SpanNearQuery.Builder builer = SpanNearQuery.newOrderedNearQuery("title");
			builer.addClause(tq1).addGap(1).setSlop(0).addClause(tq2);
			SpanNearQuery nq = builer.build();
			query = nq;
		}else if ("termRangeQuery".equals(queryType)) {//百度产品大全,百度网址大全
			/*
			 * termRangeQuery是按字符串范围来查找的，既然要确定范围，就需要比较大小，而它是按ASCII码值来比较的。
			 * 用英文按ASC码比较还有意义，比如a至g之间，但中文就毫无意义了。比如五，王之间，把李四，张三都查出来了。
			 */
			//TermRangeQuery rq = TermRangeQuery.newStringRange("creator", "五", "王", true, true);
			TermRangeQuery rq = TermRangeQuery.newStringRange("path", "D:\\nas\\samp\\lucene\\test01\\data\\file_1",
												"D:\\nas\\samp\\lucene\\test01\\data\\file_2", true, true); 
			//上面这样写,1,10都会查出来
			//比较是以整个字符串的ascii码来比较，如果要包括起止，对应值要写全。
			query = rq;
		}else if ("wildcardQuery".equals(queryType)) {
			//*表示0个或多个字符，?表示1个字符，\是转义符，所以paht中有\号的，要四个\进行转义
			WildcardQuery wq = new WildcardQuery(new Term("path","D:\\\\nas\\\\samp\\\\lucene\\\\test01\\\\data\\\\file_*.txt"));
			//WildcardQuery wq = new WildcardQuery(new Term("creator","张?")); //匹配单个字符
			//WildcardQuery wq = new WildcardQuery(new Term("title","百度?")); //匹配单个字符，必有一个，匹配：百度网
			query = wq;
		}else if ("prefixQuery".equals(queryType)) {
			//前缀查询，相当于:searchTerm*
			PrefixQuery pq = new PrefixQuery(new Term("path","D:"));
			query = pq;
		}else if ("regexpQuery".equals(queryType)) {
			//正则和wildcardQuery查询效果一样，但更精确些
			RegexpQuery pq = new RegexpQuery(new Term("creator","张."));
			query = pq;
		}else if ("fuzzyQuery".equals(queryType)) {
			/*
			 * 简单地与索引词项进行相近匹配，允许最大2个不同字符(默认就是2)。常用于拼写错误的容错。
			 * 比如：张四牛，可以匹配:张三丰,李四公,王五牛
			 */
			//FuzzyQuery fq = new FuzzyQuery(new Term("creator","张四牛"));//默认为2
			FuzzyQuery fq = new FuzzyQuery(new Term("creator","张三牛"), 1);//默认为2
			query = fq;
		}else if ("queryParser".equals(queryType)) {
			Analyzer analyzer = new IKAnalyzer(true, true, false);
			QueryParser qp = new QueryParser("title",analyzer);
			//qp.setDefaultOperator(Operator.AND);默认是OR
			query = qp.parse("产品网址");
		}else if ("multiFieldQueryParser".equals(queryType)) {
			String[] fields = {"title","creator"};
			Analyzer analyzer = new IKAnalyzer(true, true, false);
			MultiFieldQueryParser qp = new MultiFieldQueryParser(fields,analyzer);
			//qp.setDefaultOperator(Operator.OR);
			query = qp.parse("产品网址,张三丰 AND intNum:100");
		}else if ("standardQueryParser".equals(queryType)) {
			/*
			 * 会生成BooleanQuery，但似乎生成的intNum:[0 TO 100]没起作用
			 */
			String[] fields = {"title","creator"};
			Analyzer analyzer = new IKAnalyzer(true, true, false);
			StandardQueryParser sqp = new StandardQueryParser(analyzer);
			query = sqp.parse("产品网址 AND intNum:[30 TO 50]","title");
		}else{
			throw new ParseException("unsupport queryType: "+queryType);
		}
		return query;
	}
	
	public static void main(String[] args) {
		String indexDir = "D:\\nas\\samp\\lucene\\test01\\index";
		//基本类型查询
		//searchDoc(indexDir, "path","D:\\nas\\samp\\lucene\\test01\\data\\file_1.txt", 100, "termQuery");
		//searchDoc(indexDir, "creator","张三", 100, "termQuery");
		//searchDoc(indexDir, "title","百度", 100, "termQuery");
		//searchDoc(indexDir, "id","1", 100, "intQuery");
		//searchDoc(indexDir, "id","4", 100, "intQuery-range");
		//searchDoc(indexDir, "doubleNum","1.5", 100, "doubleQuery-range");
		//searchDoc(indexDir, "createTime","20180628063855", 100, "dateQuery");
		//boolean 查询
		//searchDoc(indexDir, 100, "booleanQuery");
		//PhraseQuery  
		//searchDoc(indexDir, 100, "phraseQuery"); 
		//searchDoc(indexDir, 100, "multiPhraseQuery");
		//searchDoc(indexDir, 100, "spanTermQuery");
		//searchDoc(indexDir, 100, "spanNearQuery");
		//searchDoc(indexDir, 100, "termRangeQuery");
		//searchDoc(indexDir, 100, "wildcardQuery");
		//searchDoc(indexDir, 100, "prefixQuery");
		//searchDoc(indexDir, 100, "regexpQuery");
		//searchDoc(indexDir, 100, "fuzzyQuery");
		//searchDoc(indexDir, 100, "queryParser");
		//searchDoc(indexDir, 100, "multiFieldQueryParser");
		searchDoc(indexDir, 100, "standardQueryParser");
	}
}
